package com.gitee.wsl.security.hash.bcrypt


import com.gitee.wsl.base.Objects
import com.gitee.wsl.io.Charset
import com.gitee.wsl.lang.bytearray.Bytes
import com.gitee.wsl.lang.bytearray.BytesTransformer
import com.gitee.wsl.lang.bytearray.BytesValidator
import com.gitee.wsl.lang.bytearray.secureWipe
import com.gitee.wsl.mathematics.function.random.SecureRandom
import kotlin.jvm.JvmOverloads

/**
 * The main access point the Bcrypt APIs
 */

object BCrypt {
    private val DEFAULT_CHARSET: Charset = Charset.UTF8

    /**
     * Fixed length of the bcrypt salt
     */
    const val SALT_LENGTH: Int = 16

    /**
     * Minimum allowed cost factor
     */
    const val MIN_COST: Int = 4

    /**
     * Maximum allowed cost factor
     */
    const val MAX_COST: Int = 31

    /**
     * The raw hash out length in byte
     */
    const val HASH_OUT_LENGTH: Int = 23

    /**
     * Ascii hex pointer for '2'
     */
    const val MAJOR_VERSION: Byte = 0x32

    /**
     * Ascii hex pointer for '$'
     */
    const val SEPARATOR: Byte = 0x24

    /**
     * Create a new instance of bcrypt hash with default version [Version.VERSION_2A].
     * Will throw an exception if given password is longer than the max length support for bcrypt of [Version.allowedMaxPwLength].
     *
     * @return new bcrypt hash instance
     */
    fun withDefaults(): Hasher {
        return BCrypt.Hasher(
            Version.Companion.VERSION_2A,
            SecureRandom,
            LongPasswordStrategies.strict(Version.Companion.VERSION_2A)
        )
    }

    /**
     * Create a new instance of bcrypt hash with given [Version].
     * Will throw an exception if given password is longer than the max length support for bcrypt of [Version.allowedMaxPwLength].
     *
     * @param version defines what version of bcrypt will be generated (mostly the version identifier changes)
     * @return new bcrypt hash instance
     */
    fun with(version: Version): Hasher {
        return BCrypt.Hasher(version, SecureRandom, LongPasswordStrategies.strict(version))
    }

    /**
     * Create a new instance of bcrypt hash with default version [Version.VERSION_2A].
     * The passed [SecureRandom] is used for generating the random salt.
     * Will throw an exception if given password is longer than the max length support for bcrypt of [Version.allowedMaxPwLength].
     *
     * @param secureRandom to use for random salt generation
     * @return new bcrypt hash instance
     */
    fun with(secureRandom: SecureRandom): Hasher {
        return BCrypt.Hasher(
            Version.Companion.VERSION_2A,
            secureRandom,
            LongPasswordStrategies.strict(Version.Companion.VERSION_2A)
        )
    }

    /**
     * Create a new instance of bcrypt hash with default version [Version.VERSION_2A].
     * The passed [LongPasswordStrategy] will decide what to do when the password is longer than the supported
     * [Version.allowedMaxPwLength]
     *
     * @param longPasswordStrategy decides what to do on pw that are too long
     * @return new bcrypt hash instance
     */
    fun with(longPasswordStrategy: LongPasswordStrategy): Hasher {
        return BCrypt.Hasher(
            Version.Companion.VERSION_2A,
            SecureRandom,
            longPasswordStrategy
        )
    }

    /**
     * Create a new instance with custom version and long password strategy
     *
     * @param version              defines what version of bcrypt will be generated (mostly the version identifier changes)
     * @param longPasswordStrategy decides what to do on pw that are too long
     * @return new bcrypt hash instance
     */
    fun with(version: Version, longPasswordStrategy: LongPasswordStrategy): Hasher {
        return BCrypt.Hasher(version, SecureRandom, longPasswordStrategy)
    }


    /**
     * Create a new instance with custom version, secureRandom and long password strategy
     *
     * @param version              defines what version of bcrypt will be generated (mostly the version identifier changes)
     * @param secureRandom         to use for random salt generation
     * @param longPasswordStrategy decides what to do on pw that are too long
     * @return new bcrypt hash instance
     */
    fun with(version: Version, secureRandom: SecureRandom, longPasswordStrategy: LongPasswordStrategy): Hasher {
        return BCrypt.Hasher(version, secureRandom, longPasswordStrategy)
    }

    fun verifyer(): Verifyer {
        return BCrypt.Verifyer(null,null)
    }

    /**
     * Creates a new instance of bcrypt verifier to verify a password against a given hash.
     *
     * @param version to use, also matters in  [Verifyer.verifyStrict]
     * @return new verifier instance
     */
    fun verifyer(version: Version): Verifyer {
        return BCrypt.Verifyer(version, LongPasswordStrategies.strict(version))
    }

    /**
     * Creates a new instance of bcrypt verifier to verify a password against a given hash.
     * This "verify" also respects the passed [LongPasswordStrategy] for creating the reference hash - use this
     * if you use one while hashing.
     *
     * @param version              to use, also matters in  [Verifyer.verifyStrict]
     * @param longPasswordStrategy used to create the reference hash.
     * @return new verifier instance
     */
    /**
     * Creates a new instance of bcrypt verifier to verify a password against a given hash.
     *
     * @return new verifier instance
     */
    fun verifyer(version: Version, longPasswordStrategy: LongPasswordStrategy): Verifyer {
        return BCrypt.Verifyer(version, longPasswordStrategy)
    }

    /**
     * Can create bcrypt hashes
     */
    class Hasher(
        private val version: Version,
        private val secureRandom: SecureRandom,
        private val longPasswordStrategy: LongPasswordStrategy
    ) {
        private val defaultCharset: Charset = DEFAULT_CHARSET

        /**
         * Hashes given password with the OpenBSD bcrypt schema. The cost factor will define how expensive the hash will
         * be to generate. This method will use a [SecureRandom] to generate the internal 16 byte hash.
         *
         *
         * This implementation will add a null-terminator to the password and return a 23 byte length hash in accordance
         * with the OpenBSD implementation.
         *
         *
         * The random salt will be created internally with a [SecureRandom] instance.
         *
         * @param cost     exponential cost (log2 factor) between [.MIN_COST] and [.MAX_COST] e.g. 12 --&gt; 2^12 = 4,096 iterations
         * @param password to hash, will be internally converted to an utf-8 byte array representation
         * @return bcrypt hash as a char array utf-8 encoded which includes version, cost-factor, salt and the raw hash (as radix64)
         */
        fun hashToChar(cost: Int, password: CharArray): CharArray {
            return defaultCharset.decode(hash(cost, password)).toList().toCharArray()
        }

        /**
         * Hashes given password with the OpenBSD bcrypt schema. The cost factor will define how expensive the hash will
         * be to generate. This method will use a [SecureRandom] to generate the internal 16 byte hash.
         *
         *
         * This implementation will add a null-terminator to the password and return a 23 byte length hash in accordance
         * with the OpenBSD implementation.
         *
         *
         * The random salt will be created internally with a [SecureRandom] instance.
         *
         *
         * This is the same as calling `new String(hash(cost, password), StandardCharsets.UTF-8)`
         *
         * @param cost     exponential cost (log2 factor) between [.MIN_COST] and [.MAX_COST] e.g. 12 --&gt; 2^12 = 4,096 iterations
         * @param password to hash, will be internally converted to an utf-8 byte array representation
         * @return bcrypt as utf-8 encoded String, which includes version, cost-factor, salt and the raw hash (as radix64)
         */
        fun hashToString(cost: Int, password: CharArray): CharSequence {
            return defaultCharset.decode(hash(cost, password))
        }

        /**
         * Hashes given password with the OpenBSD bcrypt schema. The cost factor will define how expensive the hash will
         * be to generate. This method will use a [SecureRandom] to generate the internal 16 byte hash.
         *
         *
         * This implementation will add a null-terminator to the password and return a 23 byte length hash in accordance
         * with the OpenBSD implementation.
         *
         *
         * The random salt will be created internally with a [SecureRandom] instance.
         *
         * @param cost     exponential cost (log2 factor) between [.MIN_COST] and [.MAX_COST] e.g. 12 --&gt; 2^12 = 4,096 iterations
         * @param password to hash, will be internally converted to an utf-8 byte array representation
         * @return bcrypt hash utf-8 encoded byte array which includes version, cost-factor, salt and the raw hash (as radix64)
         */
        fun hash(cost: Int, password: CharArray): ByteArray {
            requireNotNull(password) { "provided password must not be null" }

            var passwordBytes: ByteArray? = null
            try {
                passwordBytes = Bytes.from(password, defaultCharset).array()
                return hash(
                    cost,
                    Bytes.random(SALT_LENGTH, secureRandom).array(),
                    passwordBytes
                )
            } finally {
                Bytes.wrapNullSafe(passwordBytes).secureWipe()
            }
        }

        /**
         * Hashes given password with the OpenBSD bcrypt schema. The cost factor will define how expensive the hash will
         * be to generate. This method will use a [SecureRandom] to generate the internal 16 byte hash.
         *
         *
         * This implementation will add a null-terminator to the password and return a 23 byte length hash in accordance
         * with the OpenBSD implementation.
         *
         *
         * The random salt will be created internally with a [SecureRandom] instance.
         *
         * @param cost     exponential cost (log2 factor) between [.MIN_COST] and [.MAX_COST] e.g. 12 --&gt; 2^12 = 4,096 iterations
         * @param password the utf-8 encoded byte array representation
         * @return bcrypt hash utf-8 encoded byte array which includes version, cost-factor, salt and the raw hash (as radix64)
         */
        fun hash(cost: Int, password: ByteArray): ByteArray {
            return hash(
                cost,
                secureRandom.nextBytes(SALT_LENGTH),
                    //Bytes.random(SALT_LENGTH, secureRandom).array(),
                password
            )
        }

        /**
         * Hashes given password with the OpenBSD bcrypt schema. The cost factor will define how expensive the hash will
         * be to generate. This method will use given salt byte array
         *
         *
         * This implementation will add a null-terminator to the password and return a 23 byte length hash in accordance
         * with the OpenBSD implementation.
         *
         *
         * Note: This is part of the advanced APIs, only use if you know what you are doing.
         *
         * @param cost     exponential cost factor between [.MIN_COST] and [.MAX_COST] e.g. 12 --&gt; 2^12 = 4,096 iterations
         * @param salt     a random 16 byte long word, only used once
         * @param password the utf-8 encoded byte array representation
         * @return bcrypt hash utf-8 encoded byte array which includes version, cost-factor, salt and the raw hash (as radix64)
         */
        fun hash(cost: Int, salt: ByteArray, password: ByteArray): ByteArray {
            return version.formatter.createHashMessage(hashRaw(cost, salt, password))
        }

        /**
         * Hashes given password with the OpenBSD bcrypt schema. The cost factor will define how expensive the hash will
         * be to generate. This method will use given salt byte array
         *
         *
         * This implementation will add a null-terminator to the password and return a 23 byte length hash in accordance
         * with the OpenBSD implementation.
         *
         *
         * Note: This is part of the advanced APIs, only use if you know what you are doing.
         *
         * @param cost     exponential cost (log2 factor) between [.MIN_COST] and [.MAX_COST] e.g. 12 --&gt; 2^12 = 4,096 iterations
         * @param salt     a random 16 byte long word, only used once
         * @param password the utf-8 encoded byte array representation
         * @return the parts needed to parse the bcrypt hash message as raw byte arrays (salt, hash, cost, etc.)
         */
        fun hashRaw(cost: Int, salt: ByteArray, password: ByteArray): HashData {
            var password = password
            require(!(cost > MAX_COST || cost < MIN_COST)) { "cost factor must be between $MIN_COST and $MAX_COST, was $cost" }
            requireNotNull(salt) { "salt must not be null" }
            require(salt.size == SALT_LENGTH) { "salt must be exactly " + SALT_LENGTH + " bytes, was " + salt.size }
            requireNotNull(password) { "provided password must not be null" }

            require(!(!version.appendNullTerminator && password.isEmpty())) { "provided password must at least be length 1 if no null terminator is appended" }

            if (password.size > version.allowedMaxPwLength) {
                password = longPasswordStrategy.derive(password)
            }

            val pwWithNullTerminator: ByteArray =
                if (version.appendNullTerminator) Bytes.wrap(password).append(0.toByte()).array() else Bytes.wrap(
                    password
                ).copy().array()
            // Note: pwWithNullTerminator may be longer than allowedMaxPwLength, which is not an issue because the NUL terminator
            // only plays a part with shorter passwords that need to be cyclically repeated to provide the 72 bytes of keying
            // material required by EksBlowfish key expansion.
            try {
                val hash: ByteArray =
                    BCryptOpenBSDProtocol().cryptRaw(1L shl cost.toLong().toInt(), salt, pwWithNullTerminator)
                return HashData(
                    cost,
                    version,
                    salt,
                    if (version.useOnly23bytesForHash) Bytes.wrap(hash).resize(
                        HASH_OUT_LENGTH,
                        BytesTransformer.ResizeTransformer.Mode.RESIZE_KEEP_FROM_ZERO_INDEX
                    ).array() else hash
                )
            } finally {
                Bytes.wrapNullSafe(pwWithNullTerminator).secureWipe()
            }
        }
    }

    /**
     * Holds the raw data of a bcrypt hash
     */
    class HashData(cost: Int, version: Version, rawSalt: ByteArray, rawHash: ByteArray) {
        /**
         * The cost (log2 factor) used to create the hash
         */
        val cost: Int

        /**
         * The used version
         */
        val version: Version

        /**
         * The raw 16 bytes of the salt (not the radix64 encoded version)
         */
        val rawSalt: ByteArray

        /**
         * The raw 23 bytes of hash (not the radix64 encoded version)
         */
        val rawHash: ByteArray

        init {
            require(
                !(!Bytes.wrap(rawSalt).validate(BytesValidator.exactLength(16)) ||
                        !Bytes.wrap(rawHash).validate(
                            BytesValidator.or(
                                BytesValidator.exactLength(23),
                                BytesValidator.exactLength(24)
                            )
                        ))
            ) { "salt must be exactly 16 bytes and hash 23 bytes long" }
            this.cost = cost
            this.version = version
            this.rawSalt = rawSalt
            this.rawHash = rawHash
        }

        /**
         * Internally wipe the salt and hash byte arrays
         */
        fun wipe() {
            Bytes.wrapNullSafe(rawSalt).secureWipe()
            Bytes.wrapNullSafe(rawHash).secureWipe()
        }


        override fun equals(other: Any?): Boolean {
            if (this == other) return true
            if (other == null || this::class !== other::class) return false
            val hashData = other as HashData
            return cost == hashData.cost && version == hashData.version &&
                    Bytes.wrap(rawSalt).equalsConstantTime(hashData.rawSalt) &&
                    Bytes.wrap(rawHash).equalsConstantTime(hashData.rawHash)
        }


        override fun hashCode(): Int {
            var result: Int = Objects.hashCode(cost, version)
            result = 31 * result + rawSalt.hashCode()
            result = 31 * result + rawHash.hashCode()
            return result
        }


        override fun toString(): String {
            return "HashData{" +
                    "cost=" + cost +
                    ", version=" + version +
                    ", rawSalt=" + rawSalt.toHexString() +
                    ", rawHash=" + rawHash.toHexString() +
                    '}'
        }
    }

    /**
     * Can verify bcrypt hashes
     */
    class Verifyer(private val version: Version?,
                   private val longPasswordStrategy: LongPasswordStrategy?
    ) {
        private val defaultCharset: Charset = DEFAULT_CHARSET

        /**
         * Verify given bcrypt hash, which includes salt and cost factor with given raw password requiring a specific
         * version. If the version does not match, [Result.verified] will be false, even if the hash matches.
         *
         *
         * If given hash has an invalid format [Result.validFormat] will be false; see also
         * [Result.formatErrorMessage] for easier debugging.
         *
         *
         * Using the strict method will also require the version identifier to match, matching hash does not suffice.
         *
         * @param password   to compare against the hash
         * @param bcryptHash to compare against the password
         * @return result object, see [Result] for more info
         */
        fun verifyStrict(password: ByteArray, bcryptHash: ByteArray): Result {
            return innerVerifyBytes(password, bcryptHash, true)
        }

        /**
         * Verify given bcrypt hash, which includes salt and cost factor with given raw password.
         * The result will have [Result.verified] true if they match. If given hash has an
         * invalid format [Result.validFormat] will be false; see also [Result.formatErrorMessage]
         * for easier debugging.
         *
         * @param password   to compare against the hash
         * @param bcryptHash to compare against the password
         * @return result object, see [Result] for more info
         */
        fun verify(password: ByteArray, bcryptHash: ByteArray): Result {
            return innerVerifyBytes(password, bcryptHash, false)
        }

        /**
         * Verify given bcrypt hash, which includes salt and cost factor with given raw password requiring a specific
         * version. If the version does not match, [Result.verified] will be false, even if the hash matches.
         *
         *
         * If given hash has an invalid format [Result.validFormat] will be false; see also
         * [Result.formatErrorMessage] for easier debugging.
         *
         *
         * Using the strict method will also require the version identifier to match, matching hash does not suffice.
         *
         * @param password   to compare against the hash
         * @param bcryptHash to compare against the password
         * @return result object, see [Result] for more info
         */
        fun verifyStrict(password: CharArray, bcryptHash: CharArray): Result {
            return innerVerifyChar(password, bcryptHash, true)
        }

        /**
         * Verify given bcrypt hash, which includes salt and cost factor with given raw password.
         * The result will have [Result.verified] true if they match. If given hash has an
         * invalid format [Result.validFormat] will be false; see also [Result.formatErrorMessage]
         * for easier debugging.
         *
         * @param password   to compare against the hash
         * @param bcryptHash to compare against the password
         * @return result object, see [Result] for more info
         */
        fun verify(password: CharArray, bcryptHash: CharArray): Result {
            return innerVerifyChar(password, bcryptHash, false)
        }

        /**
         * Verify given bcrypt hash, which includes salt and cost factor with given raw password.
         * The result will have [Result.verified] true if they match. If given hash has an
         * invalid format [Result.validFormat] will be false; see also [Result.formatErrorMessage]
         * for easier debugging.
         *
         *
         * Same as calling `verify(password, bcryptHash.toCharArray())`
         *
         * @param password   to compare against the hash
         * @param bcryptHash to compare against the password (you may just pass a regular [String])
         * @return result object, see [Result] for more info
         */
        fun verify(password: CharArray, bcryptHash: CharSequence): Result {
            return innerVerifyChar(
                password,
                toCharArray(bcryptHash),
                false
            )
        }

        /**
         * Verify given bcrypt hash, which includes salt and cost factor with given raw password.
         * The result will have [Result.verified] true if they match. If given hash has an
         * invalid format [Result.validFormat] will be false; see also [Result.formatErrorMessage]
         * for easier debugging.
         *
         *
         * Same as calling `verify(Bytes.from(password, defaultCharset).array(), bcryptHash.toCharArray())`
         *
         * @param password   to compare against the hash
         * @param bcryptHash to compare against the password; here the whole bcrypt hash
         * (including salt, etc.) in its encoded form is expected not the
         * raw bytes found in [HashData.rawHash]
         * @return result object, see [Result] for more info
         */
        fun verify(password: CharArray, bcryptHash: ByteArray): Result {
             val pw = Bytes.from(password, defaultCharset)
            return innerVerifyBytes(pw.array(), bcryptHash, false)
        }

        /**
         * Verify given password against a bcryptHash with char types
         */
        private fun innerVerifyChar(password: CharArray, bcryptHash: CharArray, strict: Boolean): Result {
            var passwordBytes: ByteArray? = null
            var bcryptHashBytes: ByteArray? = null
            try {
                passwordBytes = Bytes.from(password, defaultCharset).array()
                bcryptHashBytes = Bytes.from(bcryptHash, defaultCharset).array()
                return innerVerifyBytes(passwordBytes, bcryptHashBytes, strict)
            } finally {
                Bytes.wrapNullSafe(passwordBytes).secureWipe()
                Bytes.wrapNullSafe(bcryptHashBytes).secureWipe()
            }
        }

        /**
         * Verify given password against a bcryptHash with byte types
         */
        private fun innerVerifyBytes(password: ByteArray, bcryptHash: ByteArray, strict: Boolean): Result {
            try {
                val usedVersion: Version?
                val hashData: HashData

                if (this.version == null) {
                    hashData = Version.Companion.VERSION_2A.parser.parse(bcryptHash)
                    usedVersion = hashData.version
                } else {
                    usedVersion = this.version
                    hashData = usedVersion.parser.parse(bcryptHash)
                }

                if (strict) {
                    requireNotNull(this.version) {
                        "Using strict requires to define a Version. " +
                                "Try 'BCrypt.verifier(Version.VERSION_2A)'."
                    }
                    if (hashData.version != this.version) {
                        return Result(hashData, false)
                    }
                }

                return verifyBCrypt(
                    usedVersion, determinePasswordStrategy(
                        usedVersion
                    ), password, hashData.cost, hashData.rawSalt, hashData.rawHash
                )
            } catch (e: IllegalBCryptFormatException) {
                return Result(e)
            }
        }

        private fun determinePasswordStrategy(usedVersion: Version): LongPasswordStrategy {
            val usedLongPasswordStrategy: LongPasswordStrategy?
            if (this.longPasswordStrategy == null) {
                usedLongPasswordStrategy = LongPasswordStrategies.strict(usedVersion)
            } else {
                usedLongPasswordStrategy = this.longPasswordStrategy
            }
            return usedLongPasswordStrategy
        }

        /**
         * Verify given raw byte arrays of salt, 23 byte bcrypt hash and password. This is handy if the bcrypt messages are not packaged
         * in the default Modular Crypt Format (see also [Hasher.hashRaw]).
         *
         *
         * The result will have [Result.verified] true if they match. If given hash has an
         * invalid format [Result.validFormat] will be false; see also [Result.formatErrorMessage]
         * for easier debugging.
         *
         *
         * Note: This is part of the advanced APIs, only use if you know what you are doing.
         *
         * @param password       to compare against the hash
         * @param bcryptHashData containing the data of the bcrypt hash (cost, salt, version, etc.)
         * @return result object, see [Result] for more info
         */
        fun verify(password: ByteArray, bcryptHashData: HashData): Result {
            return verify(password, bcryptHashData.cost, bcryptHashData.rawSalt, bcryptHashData.rawHash)
        }

        /**
         * Verify given raw byte arrays of salt, 23 byte bcrypt hash and password. This is handy if the bcrypt messages are not packaged
         * in the default Modular Crypt Format (see also [Hasher.hashRaw]).
         *
         *
         * The result will have [Result.verified] true if they match. If given hash has an
         * invalid format [Result.validFormat] will be false; see also [Result.formatErrorMessage]
         * for easier debugging.
         *
         *
         * Note: This is part of the advanced APIs, only use if you know what you are doing.
         *
         * @param password             to compare against the hash
         * @param cost                 cost (log2 factor) which was used to create the hash
         * @param salt                 16 byte raw hash value (not radix64 version) which was used to create the hash
         * @param rawBcryptHash23Bytes 23 byte raw bcrypt hash value (not radix64 version)
         * @return result object, see [Result] for more info
         */
        fun verify(password: ByteArray, cost: Int, salt: ByteArray, rawBcryptHash23Bytes: ByteArray): Result {
            val usedVersion = this.version ?: Version.Companion.VERSION_2A
            return verifyBCrypt(
                usedVersion,
                determinePasswordStrategy(usedVersion),
                password,
                cost,
                salt,
                rawBcryptHash23Bytes
            )
        }

        companion object {
            /**
             * Convert a string type to char array in the most efficient manner.
             */
            private fun toCharArray(charSequence: CharSequence): CharArray {
                if (charSequence is String) {
                    return charSequence.toCharArray()
                } else {
                    val buffer = CharArray(charSequence.length)
                    for (i in 0..<charSequence.length) {
                        buffer[i] = charSequence[i]
                    }
                    return buffer
                }
            }

            /**
             * Raw bcrypt verification
             */
            private fun verifyBCrypt(
                version: Version, longPasswordStrategy: LongPasswordStrategy,
                password: ByteArray, cost: Int, salt: ByteArray, rawBcryptHash23Bytes: ByteArray
            ): Result {
                val hashData: HashData = with(
                    version,
                    longPasswordStrategy
                )
                    .hashRaw(cost, salt, password)
                return Result(
                    hashData,
                    Bytes.wrap(hashData.rawHash).equalsConstantTime(rawBcryptHash23Bytes)
                )
            }
        }
    }

    /**
     * Result of a bcrypt hash verification
     */
    class Result private constructor(
        /**
         * The parts of the modular crypt format (salt, raw hash, cost factor, version)
         */
        val details: HashData?,
        /**
         * If the given format was valid. E.g. '$2a$10$k87L/MF28Q673VKh8/cPi.SUl7MU/rWuSiIDDFayrKk/1tBsSQu4u'
         */
        val validFormat: Boolean,
        /**
         * If the given password matches the hash
         */
        val verified: Boolean,
        /**
         * Optional error message if [.validFormat] is false
         */
        val formatErrorMessage: String?
    ) {
        internal constructor(e: IllegalBCryptFormatException) : this(null, false, false, e.message)

        internal constructor(details: HashData?, verified: Boolean) : this(details, true, verified, null)


        override fun equals(o: Any?): Boolean {
            if (this == o) return true
            if (o == null || this::class !== o::class) return false
            val result = o as Result
            return validFormat == result.validFormat && verified == result.verified &&
                    Objects.equals(details, result.details) &&
                    Objects.equals(formatErrorMessage, result.formatErrorMessage)
        }


        override fun hashCode(): Int {
            return Objects.hashCode(details, validFormat, verified, formatErrorMessage)
        }


        override fun toString(): String {
            return "Result{" +
                    "details=" + details +
                    ", validFormat=" + validFormat +
                    ", verified=" + verified +
                    ", formatErrorMessage='" + formatErrorMessage + '\'' +
                    '}'
        }
    }

    /**
     * The supported version identifiers for bcrypt according to the modular crypt format.
     *
     *
     * See: [modular_crypt_format](https://passlib.readthedocs.io/en/stable/modular_crypt_format.html)
     */
    class Version(
        /**
         * Version identifier byte array, eg.{0x32, 0x61} for '2a'
         */
        val versionIdentifier: ByteArray?,
        /**
         * Due to a bug the OpenBSD implementation only uses 23 bytes (184 bit) of the possible 24 byte output from
         * blowfish. Set this to false if you want the full 24 byte out (which makes it incompatible with most other impl)
         */
        val useOnly23bytesForHash: Boolean,
        /**
         * Since OpenBSD bcrypt version $2a$ a null-terminator byte must be appended to the hash. This flag decides if
         * that will be done during hashing.
         */
        val appendNullTerminator: Boolean,
        /**
         * The max allowed length of password in bcrypt, longer than that [LongPasswordStrategy] will be activated.
         * Usual lengths are between 50 and 72 bytes, most often are 56, 71 or 72 bytes.
         * See [security.stackexchange.com](https://security.stackexchange.com/a/39851)
         */
        val allowedMaxPwLength: Int,
        /**
         * The formatter for the bcrypt message digest
         */
        val formatter: BCryptFormatter = DEFAULT_FORMATTER,
        /**
         * The parser used to parse a bcrypt message
         */
        val parser: BCryptParser = DEFAULT_PARSER
    ) {

        private constructor(versionIdentifier: ByteArray?, formatter: BCryptFormatter, parser: BCryptParser) : this(
            versionIdentifier,
            true,
            true,
            MAX_PW_LENGTH_BYTE,
            formatter,
            parser
        )

        /**
         * Create a new version. Only use this if you are know what you are doing, most common versions are already available with
         * [Version.VERSION_2A], [Version.VERSION_2Y] etc.
         *
         * @param versionIdentifier     version as UTF-8 encoded byte array, e.g. '2a' = new byte[]{0x32, 0x61}, do not included the separator '$'
         * @param useOnly23bytesForHash set to false if you want the full 24 byte out for the hash (otherwise will be truncated to 23 byte according to OpenBSD impl)
         * @param appendNullTerminator  as defined in $2a$+ a null terminator is appended to the password, pass false if you want to avoid this
         * @param allowedMaxPwLength    the max allowed length of password in bcrypt, longer than that [LongPasswordStrategy] will be activated
         * @param formatter             the formatter responsible for formatting the out hash message digest
         * @param parser                responsible for parsing the message format
         */
        init {
            require(allowedMaxPwLength <= MAX_PW_LENGTH_BYTE) { "allowed max pw length cannot be gt " + MAX_PW_LENGTH_BYTE }
        }


        override fun equals(o: Any?): Boolean {
            if (this == o) return true
            if (o == null || this::class !== o::class) return false
            val version = o as Version
            return useOnly23bytesForHash == version.useOnly23bytesForHash && appendNullTerminator == version.appendNullTerminator && allowedMaxPwLength == version.allowedMaxPwLength &&
                    Objects.equals(versionIdentifier, version.versionIdentifier)
        }


        override fun hashCode(): Int {
            var result: Int = Objects.hash(useOnly23bytesForHash, appendNullTerminator, allowedMaxPwLength)
            result = 31 * result + Objects.hashCode(versionIdentifier)
            return result
        }


        override fun toString(): String {
            return "$$versionIdentifier$"
        }

        companion object {
            private val DEFAULT_FORMATTER: BCryptFormatter =
                BCryptFormatter.Default(Radix64Encoder.Default(), DEFAULT_CHARSET)
            private val DEFAULT_PARSER: BCryptParser =
                BCryptParser.Default(Radix64Encoder.Default(), DEFAULT_CHARSET)

            /**
             * Absolutely maximum length bcrypt can support (18x32bit).
             * Shorter passwords are repeated cyclically, possibly with NUL bytes separating each occurrence
             * (but the NUL byte should not count against the limit: if the password is exactly this length
             * then all characters are used and no repetition needs to happen, so no NUL needs to be inserted).
             */
            const val MAX_PW_LENGTH_BYTE: Int = 72

            /**
             * $2a$
             *
             *
             * The original specification did not define how to handle non-ASCII character, nor how to handle a null
             * terminator. The specification was revised to specify that when hashing strings:
             * - the string must be UTF-8 encoded
             * - the null terminator must be included
             */
            val VERSION_2A: Version = Version(
                byteArrayOf(
                    MAJOR_VERSION,
                    0x61
                ),
                DEFAULT_FORMATTER,
                DEFAULT_PARSER
            )

            /**
             * $2b$ (2014/02)
             *
             *
             * A bug was discovered in the OpenBSD implementation of bcrypt. They were storing the length of their strings
             * in an unsigned char (i.e. 8-bit Byte). If a password was longer than 255 characters, it would overflow
             * and wrap at 255. To recognize possible incorrect hashes, a new version was created.
             */
            val VERSION_2B: Version = Version(
                byteArrayOf(
                    MAJOR_VERSION,
                    0x62
                ),
                DEFAULT_FORMATTER,
                DEFAULT_PARSER
            )

            /**
             * $2x$ (2011)
             *
             *
             * Due to a bug in crypt_blowfish, a PHP implementation of BCrypt, a new version string was introduced to
             * recognize old hashes. It was mis-handling characters with the 8th bit set. Nobody else, including canonical
             * OpenBSD, adopted the idea of 2x/2y so this version marker change was limited to crypt_blowfish.
             */
            val VERSION_2X: Version = Version(
                byteArrayOf(
                    MAJOR_VERSION,
                    0x78
                ),
                DEFAULT_FORMATTER,
                DEFAULT_PARSER
            )

            /**
             * $2y$ (2011)
             *
             *
             * See [.VERSION_2X]
             */
            val VERSION_2Y: Version = Version(
                byteArrayOf(
                    MAJOR_VERSION,
                    0x79
                ),
                DEFAULT_FORMATTER,
                DEFAULT_PARSER
            )

            /**
             * $2y$ (2011) without the null terminator
             *
             *
             * See [.VERSION_2Y]
             */
            val VERSION_2Y_NO_NULL_TERMINATOR: Version = Version(
                byteArrayOf(
                    MAJOR_VERSION,
                    0x79
                ),
                useOnly23bytesForHash = true,
                appendNullTerminator = false,
                allowedMaxPwLength = MAX_PW_LENGTH_BYTE,
                formatter = DEFAULT_FORMATTER,
                parser = DEFAULT_PARSER
            )

            /**
             * This mirrors how Bouncy Castle creates bcrypt hashes: with 24 byte out and without null-terminator. Gets a fake
             * version descriptor.
             */
            val VERSION_BC: Version = Version(
                byteArrayOf(
                    MAJOR_VERSION,
                    0x63
                ),
                useOnly23bytesForHash = false,
                appendNullTerminator = false,
                allowedMaxPwLength = MAX_PW_LENGTH_BYTE,
                formatter = DEFAULT_FORMATTER,
                parser = DEFAULT_PARSER
            )

            /**
             * List of supported versions
             */
            val SUPPORTED_VERSIONS: List<Version> = listOf(
                    VERSION_2A,
                    VERSION_2B,
                    VERSION_2X,
                    VERSION_2Y
            )
        }
    }
}
